package buxter.api.commons.signature;


import java.security.MessageDigest;
import java.util.Random;

import org.apache.log4j.Logger;
import org.apache.xerces.impl.dv.util.HexBin;

import buxter.api.commons.ApiException;


/**
 * This class produces a signature using:
 * - apiConfigurationId
 * - requestName
 * - parameterValues
 * - sharedSecret
 * - 64-bit salt
 * 
 * The components are concatenated with a separator in between,
 * SHA1-hashed, converted to HEX, and appended to the salt with a separator.
 * 
 * Thus:
 * 
 *  signature = SALT + "#" + SHA1(sha1(apiConfigurationId + "#" + requestName + "#" + parameterValues + "#" + sharedSecret + "#" + salt))
 *  
 * Where SALT = hex(64-bit random)
 * 
 * <p>TODO: a timestamp could be added for extra security and to prevent signature reuse.
 * 
 * @author thijs.reus
 * @author andrei.martchouk
 * @author matthias.gall
 * @author natasha.martchouk
 *
 */
public class RequestSignatureHandlerSHA1 implements IRequestSignatureHandler {

	/** Keep a logger */
	private static final Logger logger = Logger.getLogger(RequestSignatureHandlerSHA1.class);
	
	/** This is the salt size (in bytes) */
	public static final int SALT_SIZE = 8;

	/** Keep a random generator for the salt */
	private final Random saltRandomGenerator = new Random();
	
	private String sharedSecret;
	
	private String apiConfigurationId;

	/* (non-Javadoc)
	 * @see facebook.wallet.api.util.IRequestSignatureHandler#createSignature(java.lang.String, java.lang.String)
	 */
	public String createSignature(final String requestName, final String parameterValues) throws ApiException {
		return internalCreateSignature(getApiConfigurationId(), getSharedSecret(), requestName, parameterValues, createSalt());
	}

	/* (non-Javadoc)
	 * @see facebook.wallet.api.util.IRequestSignatureHandler#validateSignature(java.lang.String, java.lang.String, java.lang.String)
	 */
	public boolean validateSignature(final String requestName, final String parameterValues, final String signature) throws ApiException {
		// Extract the salt
		final String[] components = signature.split(SEPARATOR);
		if (components == null || components.length != 2) {
			throw new ApiException("Signature has invalid syntax");
		}
		final String salt = components[0];
		// Check if it's equal
		if (!internalCreateSignature(getApiConfigurationId(), getSharedSecret(), requestName, parameterValues, salt).equalsIgnoreCase(signature)) {
			throw new ApiException("Signature is invalid");
		}
		// All ok
		return true;
	}
	
	/* (non-Javadoc)
	 * @see facebook.wallet.api.util.IRequestSignatureHandler#concatenate(java.lang.Object[])
	 */
	public String concatenate(Object... parameterValues) {
		final StringBuilder result = new StringBuilder();
		if (parameterValues != null) {
			boolean firstElement = true;
			for (final Object element : parameterValues) {
				if (!firstElement) {
					result.append(SEPARATOR);
				}
				if (element != null) {
					result.append(element);
				}
				firstElement = false;
			}
		}
		logger.debug("Concatenated Params: "+result.toString());
		return result.toString();
	}

	/**
	 * Creates a signature with the given parameters.
	 * 
	 * @param apiConfigurationId
	 * @param requestName
	 * @param parameterValues
	 * @param salt
	 * @return
	 * @throws AuthenticationFailedException
	 */
	private String internalCreateSignature(final String apiConfigurationId, final String sharedSecret, final String requestName, final String parameterValues, final String salt) throws ApiException {
		// Check input parameters
		if (requestName == null) {
			throw new IllegalArgumentException("RequestName may not be null");
		}
		if (parameterValues == null) {
			throw new IllegalArgumentException("ParameterValues may not be null");
		}
		if (salt == null) {
			throw new IllegalArgumentException("Salt may not be null");
		}
		// Compose the signature base string
		final String toBeHashed = apiConfigurationId + SEPARATOR + requestName + SEPARATOR + parameterValues + SEPARATOR + sharedSecret + SEPARATOR + salt;
		logger.debug("Plain token:" + toBeHashed);
		// Create the hash
		final String sha1Hash;
		try {
			sha1Hash= createSHA1(toBeHashed);
		} catch (Exception e) {
			logger.error("Could not generate SHA1 hash: " + e);
			throw new ApiException("Could not generate SHA1 hash");
		}
		// Concatenate
		String sig = salt + SEPARATOR + sha1Hash;
		logger.debug("Encrypted token:" + sig);
		return sig;
	}
	
	/**
	 * Creates a salt used for the signature.
	 * 
	 * @return
	 */
	private String createSalt() {
		final byte[] rawSalt = new byte[SALT_SIZE];
		saltRandomGenerator.nextBytes(rawSalt);
		return HexBin.encode(rawSalt);
	}
	
	/**
	 * Creates a SHA1 hash of the given input, and HEX-encodes it.
	 * 
	 * @param input
	 * @return
	 */
	private String createSHA1(final String input) throws Exception {
		final MessageDigest sha1 = MessageDigest.getInstance("SHA1");
		sha1.update(input.getBytes());
		return HexBin.encode(sha1.digest());
	}
	
	public String getSharedSecret() {
		return sharedSecret;
	}

	public void setSharedSecret(String sharedSecret) {
		this.sharedSecret = sharedSecret;
	}
	
	public String getApiConfigurationId() {
		return apiConfigurationId;
	}

	public void setApiConfigurationId(String apiConfigurationId) {
		if(logger.isDebugEnabled()){
			logger.debug("setting apiConfigurationId: " + apiConfigurationId);
		}
		this.apiConfigurationId = apiConfigurationId;
	}

}
